Lead Callback
The lead callback endpoint lets any system (a lead vendor, a lead-distribution platform, a CRM or your own backend) send residential solar leads into Merdeka. Each lead is created or updated in real time.
Each lead is a single POST with a flat JSON body. Merdeka matches it against existing records, so posting the same lead twice updates it rather than creating a duplicate.
Endpoint
| Environment | URL |
|---|---|
| Production (live) | POST https://externals.merdeka.app/api/v1/lead/callback |
| Development (sandbox) | POST https://externals-dev.merdeka.nvpdev.tech/api/v1/lead/callback |
💡 Send to Development first. Once leads show up correctly in the sandbox, switch the URL to Production.
Authentication
Every request must carry a Merdeka bearer token.
1. Get a token from the login endpoint (see Quick Start Push for details):
POST https://api.merdeka.app/api/v1/auth/email/login
{
"email": "your-integration-user@example.com",
"password": "your-password"
}
2. Send it on every callback:
Authorization: Bearer <your_token>
Content-Type: application/json
The token also lets Merdeka run lead-assignment rules on new leads. Leads that arrive without it can't be auto-assigned.
📩 Need an integration user? Email: merdeka@sunderenergy
Sending a Lead
Whatever system you send from, set up the request like this:
- URL: the endpoint for your environment (see above).
- Method:
POST. - Headers:
Authorization: Bearer <your_token>andContent-Type: application/json. - Body: a flat JSON object, one lead per request. Map your fields to the Merdeka field names in the Field Reference below.
- Success check: treat the request as successful only when
data.leadIdis present in the response (see Responses).
Example Request
curl -X POST "https://externals-dev.merdeka.nvpdev.tech/api/v1/lead/callback" \
-H "Authorization: Bearer <your_token>" \
-H "Content-Type: application/json" \
-d @lead.json
{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_1": "+1 (555) 010-0100",
"address_1": "123 Main St",
"city": "Springfield",
"state": "IL",
"postal_code": "62704",
"country": "USA",
"homeowner": "Yes",
"electric_company_name": "Ameren Illinois",
"electric_monthly_amount": "$185.00",
"lead_source": "Digital",
"lead_source_detail": "Google Search",
"lead_level": 1,
"lead_price": 45.0,
"is_exclusive": "Exclusive",
"vendor_id_cslr": "VENDOR-001",
"vendor_name": "Acme Leads",
"trustedform_cert_url": "https://cert.trustedform.com/abc123",
"tcpa_opt_in_cslr": "Yes",
"consent_status_cslr": true,
"utm_source": "google",
"utm_medium": "cpc",
"campaign_id": "spring-2026",
"landing_url": "https://example.com/solar",
"is_test": false
}
Field Reference
All fields are optional at the API level, but Merdeka uses the Recommended fields to match and route leads. Send them whenever you have them.
Values marked Configured list must exactly match a value set up in Merdeka. An unknown value is ignored rather than rejected. Ask the Merdeka team for the current list before you go live.
Contact
| Field | Type | Recommended | Notes |
|---|---|---|---|
first_name | string | ✅ | |
last_name | string | ✅ | |
email | string | ✅ | Used to match existing contacts at the same address. |
phone_1 | string | ✅ | Primary phone. Any format; Merdeka compares digits only. |
phone_2 | string | Secondary phone. | |
homeowner | string | Ownership status of the contact (e.g. Yes / No). | |
how_did_you_hear_about_us | string |
Property
| Field | Type | Recommended | Notes |
|---|---|---|---|
address_1 | string | ✅ | Street address. Used to match existing properties. |
address_2 | string | Unit / apartment. | |
city | string | ✅ | |
state | string | ✅ | Two-letter US state code, e.g. CA. |
postal_code | string | ✅ | Used to match existing properties. |
country | string | Defaults to USA. | |
electric_company_name | string | Utility company, free text. | |
electric_monthly_amount | string or number | Average monthly bill. Currency symbols and commas are stripped ("$1,200" → 1200). | |
property_sun_exposure | string | ||
property_type | string | Configured list. | |
roof_type | string | Configured list. |
Lead
| Field | Type | Recommended | Notes |
|---|---|---|---|
lead_source | string | ✅ | Configured list. Also part of how Merdeka recognises a repeat lead (see Duplicate handling). |
lead_source_detail | string | Configured list. | |
lead_level | integer | ✅ | Routing tier. Defaults to 1. Send a number (1), not a string ("1"). |
lead_price | number | Price paid for the lead. | |
is_exclusive | string | "Shared" marks the lead as shared; any other value marks it exclusive. | |
product_type | string | ||
company_brand | string | Configured list. | |
sdr_channel | string | Configured list. | |
credit_score | string | Configured list (score band). | |
comments | string | Free-text notes. | |
promo_code_spwr | string | Promo code. | |
is_test | boolean | Send true for test leads. Any non-empty value counts as true, so omit the field (or send false) for real leads. |
Vendor & compliance
| Field | Type | Recommended | Notes |
|---|---|---|---|
vendor_id_cslr | string | Your vendor ID. | |
vendor_name | string | ||
trustedform_cert_url | string | ✅ | TrustedForm certificate URL. |
tcpa_opt_in_cslr | string | ✅ | TCPA opt-in value as captured. |
consent_status_cslr | boolean | true when consent was given. |
Attribution
| Field | Type | Notes |
|---|---|---|
utm_source | string | |
utm_medium | string | |
utm_campaign | string | |
utm_content | string | |
utm_term | string | |
utm_id | string | Also stored as the ambassador ID. |
campaign_id | string | Campaign identifier. |
omnichannel_campaign | string | |
landing_url | string | |
conversion_url | string | |
referrer_url | string | |
google.clid | string | Google click ID, sent as a nested object: "google": { "clid": "..." }. |
Referral
| Field | Type | Notes |
|---|---|---|
referred_by_first_name_cslr | string | |
referred_by_last_name_cslr | string | |
referred_by_email | string | |
referred_by_phone | string | |
maa | string | MAA recipient. |
Sales assignment (optional)
| Field | Type | Notes |
|---|---|---|
setter | string | Setter email. setter_email is also accepted. |
closer | string | Closer email. closer_email is also accepted. |
setter_team | string | |
closer_team | string |
Duplicate Handling
Merdeka matches each callback in three steps, so re-sending a lead updates it instead of creating a copy:
- Property: an exact match on
address_1+address_2+postal_code. - Contact (at that property): first by
email, then byphone_1/phone_2(digits only). - Lead: the same contact with the same
lead_source.
If nothing matches, a new record is created at that step.
An update overwrites the lead with the values in the new callback. If you leave out a configured-list field (lead_source_detail, company_brand, sdr_channel, credit_score, property_type, roof_type), its existing value is cleared, and is_exclusive, is_test and consent_status_cslr fall back to their defaults. Always send the full lead, not just the fields that changed.
Contact attribution (utm_*, lead_source, lead_source_detail on the contact) is captured only the first time a contact is created and is not overwritten later.
Responses
Responses use the standard Merdeka envelope.
Success: 201 Created
{
"message": "Success",
"status": 201,
"data": {
"leadId": "5d0c9a7e-2f1b-4c3e-9a8d-1b2c3d4e5f60",
"leadCreated": true,
"account": { "id": "…", "address1": "123 Main St", "zipCode": "62704" },
"contact": { "id": "…", "firstName": "John", "lastName": "Doe" }
}
}
leadCreated: true: a new lead was created.leadCreated: false: an existing lead was updated (leadIdis the existing lead).
Processing failure: still 201, but data is null:
{
"message": "Success",
"status": 201,
"data": null
}
A failed lead still returns HTTP 201. Check that data.leadId is present before treating a lead as accepted; don't rely on the status code alone.
Authentication failure: 401 Unauthorized. The token is missing, invalid or expired. Log in again and update the Authorization header in your flow.
🛠️ Troubleshooting
- 401 Unauthorized: token missing or expired. Re-login and update the header.
data: null: the lead failed to process. Check the payload is valid JSON with flat field names, then send us the payload.- Lead created twice: the address, email/phone or
lead_sourcediffered between the two callbacks (see Duplicate Handling). - Source, brand or credit band missing in Merdeka: the value didn't exactly match the configured list.
- Lead routed to the wrong queue:
lead_levelwas sent as a string. Send it as a number.
📨 Still unclear? Send us your payload and response and we'll help you debug.